package eu.catengine.scene;

import javax.microedition.khronos.opengles.GL10;

import android.view.KeyEvent;
import eu.catengine.CatEngineCore;
import eu.catengine.common.iface.IDisposable;
import eu.catengine.common.manager.AnimationManager;
import eu.catengine.common.manager.AudioManager;
import eu.catengine.common.manager.DataManager;
import eu.catengine.common.manager.FontManager;
import eu.catengine.common.manager.SceneManager;
import eu.catengine.common.manager.ScreenManager;
import eu.catengine.common.manager.TextureManager;
import eu.catengine.common.util.Interpolator;
import eu.catengine.primitive.Color;

/**
 * Klasa odpowiadająca za wyświetlanie wszystkich obiektów na ekranie. <br>
 * Chcąc uruchomić silnik należy stworzyć nową scenę na podstawie tej klasy.
 * 
 * @author mbienkowski
 */
public abstract class Scene implements IDisposable
{
    // Important
    private Camera mCamera;
    private String mSceneName;

    // Layers
    private LayerTree mBackgroundTree;
    private LayerTree mSceneTree;
    private LayerTree mHudTree;
    private Color mBackgroundColor = new Color(0, 0, 0, 1);

    // Interpolators
    private Interpolator mInterpolator = new Interpolator(this);

    // Managers
    private ScreenManager mScreenManager = ScreenManager.getInstance();
    private SceneManager mSceneManager = SceneManager.getInstance();
    private TextureManager mTextureManager = TextureManager.getInstance();
    private AnimationManager mAnimationManager = AnimationManager.getInstance();
    private AudioManager mAudioManager = AudioManager.getInstance();
    private FontManager mFontManager = FontManager.getInstance();
    private DataManager mDataManager = DataManager.getInstance();

    // Game speed and game update control
    private boolean mSceneIsRunning = true;
    private long mElapsedTime = 0;
    private float mSceneSpeed = 1.0f;

    private long mScenePauseDuration = 0;
    private long mScenePauseStart = 0;
    private long mScenePauseStop = 0;
    private long mSceneStartTime = 0;

    private long mLastFrameDuration;
    private long mLastUpdateTime;
    private long mAccumulator;
    private long mMaxTimeAccumulator;
    private long mFrameDuration;
    private float mFrameDurationIncSpeed;
    private float mFrameDurationDelta;

    /**
     * Tworzy scenę.<br>
     * W przypadku dziedziczenia klasy Scene należy umieścić
     * <code>super(sceneName)</code> jako pierwszą linijkę w konstruktorze.
     * 
     * @param sceneName
     *            <code>String</code> Nazwa tworzonej sceny.
     */
    public Scene(String sceneName)
    {
	mLastFrameDuration = 0;
	mLastUpdateTime = 0;
	mAccumulator = 0;
	mMaxTimeAccumulator = 1000;
	mSceneStartTime = System.currentTimeMillis();
	mFrameDuration = CatEngineCore.getFrameDuration();

	mSceneName = sceneName;
	mCamera = new Camera(this);
	mBackgroundTree = new LayerTree(this, "background");
	mSceneTree = new LayerTree(this, "root");
	mHudTree = new LayerTree(this, "hud");
    }

    /**
     * Aktualizacja obiektów sceny.<br>
     * Zasadniczo pętla główna gry.<br>
     * <b>Metoda wywoływana automatycznie.<br>
     * <br>
     * NIE WYWOŁOWAĆ RĘCZNIE.</b>
     */
    public void updateRunningScene()
    {
	if (!mSceneIsRunning)
	{
	    return;
	}

	mLastFrameDuration = System.currentTimeMillis() - mSceneStartTime - mLastUpdateTime;
	mLastUpdateTime += mLastFrameDuration;
	mAccumulator += mLastFrameDuration - mScenePauseDuration;
	mAccumulator = Math.min(mMaxTimeAccumulator, Math.max(0, mAccumulator));
	mFrameDurationIncSpeed = mFrameDuration / Math.abs(mSceneSpeed);
	mFrameDurationDelta = mFrameDuration * Math.signum(mSceneSpeed);
	mScenePauseDuration = 0;

	while (mAccumulator >= mFrameDurationIncSpeed)
	{
	    mElapsedTime += mFrameDurationDelta;

	    mCamera.update();
	    mInterpolator.update();
	    mBackgroundTree.updateObjects(mElapsedTime);
	    mHudTree.updateObjects(mElapsedTime);
	    mSceneTree.updateObjects(mElapsedTime);

	    updateScene(mFrameDurationDelta, mElapsedTime);

	    mAccumulator -= mFrameDurationIncSpeed;
	}
    }

    /**
     * Renderowanie obiektów sceny.<br>
     * <b>Metoda wywoływana automatycznie.<br>
     * <br>
     * NIE WYWOŁOWAĆ RĘCZNIE.</b>
     */
    public void drawScene(GL10 gl)
    {
	// Clear scene
	gl.glClearColor(mBackgroundColor.red, mBackgroundColor.green, mBackgroundColor.blue, mBackgroundColor.alpha);
	gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
	// Render background
	mBackgroundTree.renderObjects(gl);
	// Move left top corner of camera to specified point
	gl.glTranslatef(-mCamera.getX(), mScreenManager.getScreenHeight() - mCamera.getY(), 0f);
	// Render all objects from the scene
	mSceneTree.renderObjects(gl);
	// Fixed position for HUD elements
	gl.glLoadIdentity();
	// Render hud to the screen
	mHudTree.renderObjects(gl);
	// Get back to camera position
	gl.glTranslatef(-mCamera.getX(), mScreenManager.getScreenHeight() - mCamera.getY(), 0f);
	// Reset the matrix
	gl.glLoadIdentity();
    }

    // Getters and setters

    /**
     * Zwraca obiekt kamery w scenie.
     */
    public Camera getCamera()
    {
	return mCamera;
    }

    /**
     * Zwraca nazwę sceny.
     */
    public String getSceneName()
    {
	return mSceneName;
    }

    /**
     * Zwraca drzewo do przechowywania obiektów tła.<br />
     * Elementy tła pozostają widoczne na ekranie mimo ruchów kamery. <br>
     * Pozycjonowanie odbywa się względem punktu (0, 0), który znajduje się w lewym
     * dolnym rogu ekranu.
     */
    public LayerTree getBackgroundTree()
    {
	return mBackgroundTree;
    }

    /**
     * Zwraca drzewo do przechowywania obiektów sceny.
     */
    public LayerTree getSceneTree()
    {
	return mSceneTree;
    }

    /**
     * Zwraca drzewo do przechowywania obiektów menu.<br>
     * Elementy typu HUD pozostają widoczne na ekranie mimo ruchów kamery. <br>
     * Pozycjonowanie odbywa się względem punktu (0, 0), który znajduje się w lewym
     * dolnym rogu ekranu.
     */
    public LayerTree getHudTree()
    {
	return mHudTree;
    }

    /**
     * Zwraca interpolator obiektów sceny.
     */
    public Interpolator getInterpolator()
    {
	return mInterpolator;
    }

    /**
     * Zwraca obiekt managera ekranu.
     */
    public ScreenManager getScreenManager()
    {
	return mScreenManager;
    }

    /**
     * Zwraca obiekt managera sceny.
     */
    public SceneManager getSceneManager()
    {
	return mSceneManager;
    }

    /**
     * Zwraca obiekt managera tekstur.
     */
    public TextureManager getTextureManager()
    {
	return mTextureManager;
    }

    /**
     * Zwraca obiekt managera animacji.
     */
    public AnimationManager getAnimationManager()
    {
	return mAnimationManager;
    }

    /**
     * Zwraca obiekt managera dzwieków.
     */
    public AudioManager getAudioManager()
    {
	return mAudioManager;
    }

    /**
     * Zwraca obiekt managera czcionek.
     */
    public FontManager getFontManager()
    {
	return mFontManager;
    }

    /**
     * Zwraca obiekt managera danych.
     */
    public DataManager getDataManager()
    {
	return mDataManager;
    }

    /**
     * Zwraca true/false w zależności czy scena się aktualizuje.
     */
    public boolean getSceneIsRunning()
    {
	return mSceneIsRunning;
    }

    /**
     * Metoda przypisuje scenie true/false w zależności od tego czy ma się
     * aktualizować lub nie.
     */
    public void setSceneIsRunning(boolean value)
    {
	if (mSceneIsRunning == value)
	{
	    return;
	}

	mSceneIsRunning = value;

	if (value)
	{
	    mScenePauseStop = System.currentTimeMillis();
	    mScenePauseDuration = mScenePauseStop - mScenePauseStart;
	}
	else
	{
	    mScenePauseDuration = 0;
	    mScenePauseStart = System.currentTimeMillis();
	}
    }

    /**
     * Ustawia przyspieszenie czasu.<br>
     * <br>
     * Dla wartości 1.0f czas płynie normalnie, dla 2.0f czas płynie dwa razy
     * szybciej.<br>
     * Można stosować negatywne wartości.
     * 
     * @param sceneSpeed
     *            <code>float</code> Przyspieszenie czasu sceny.
     */
    public void setSceneSpeed(float sceneSpeed)
    {
	this.mSceneSpeed = sceneSpeed;
    }

    /**
     * Zwraca wartość przyspieszenia sceny.
     */
    public float getSceneSpeed()
    {
	return mSceneSpeed;
    }

    /**
     * Zwraca czas jak do tej pory upłynął w scenie.
     */
    public long getElapsedTime()
    {
	return mElapsedTime;
    }

    /**
     * Ustawia kolor tła dla sceny.
     */
    public void setBackgroundColor(Color color)
    {
	mBackgroundColor = color;
    }

    /**
     * Ustawia kolor tła dla sceny.
     */
    public void setBackgroundColor(float red, float green, float blue)
    {
	mBackgroundColor.red = red;
	mBackgroundColor.green = green;
	mBackgroundColor.blue = blue;
    }

    /**
     * Ustawia kolor tła dla sceny.
     */
    public void setBackgroundScene(float red, float green, float blue, float alpha)
    {
	mBackgroundColor.red = red;
	mBackgroundColor.green = green;
	mBackgroundColor.blue = blue;
	mBackgroundColor.alpha = alpha;
    }

    /**
     * Zwraca kolor tła sceny.
     */
    public Color getBackgroundColor()
    {
	return mBackgroundColor;
    }

    @Override
    public void dispose()
    {
	mCamera.dispose();
	mInterpolator.dispose();
	mBackgroundTree.dispose();
	mSceneTree.dispose();
	mHudTree.dispose();

	mCamera = null;
	mSceneName = null;
	mBackgroundTree = null;
	mSceneTree = null;
	mHudTree = null;
	mInterpolator = null;

	mScreenManager = null;
	mSceneManager = null;
	mTextureManager = null;
	mAnimationManager = null;
	mAudioManager = null;
	mFontManager = null;
	mDataManager = null;
    }

    // Abstract methods

    /**
     * Wykonuje się w momencie, gdy zostanie wykryta akcja na ekranie.
     */
    public abstract void onPointerDown(TouchPointer pointer);

    /**
     * Wykonuje się w momencie, gdy wskaźnik porusza po ekranie.
     * 
     * @param pointer
     *            aktualny wskaźnik.
     * @param historicalPointer
     *            ostatnio rejerestowany wskaźnik.
     */
    public abstract void onPointerMove(TouchPointer pointer, TouchPointer historicalPointer);

    /**
     * Wykonuje się w momencie, gdy wskaźnik przestaje dotykać ekranu.
     * 
     * @param pointer
     *            podnoszony wskaźnik.
     */
    public abstract void onPointerUp(TouchPointer pointer);

    /**
     * Wykonuje się w momencie, gdy scena zmiania stan na nieaktywną.
     */
    public abstract void onChangeToInactiveScene();

    /**
     * Wykonuje się w momencie, gdy scena zmiania stan na aktywną.
     */
    public abstract void onChangeToActiveScene();

    /**
     * Metoda wywoływana w celu aktualizacji wszystkich elementów w grze.
     * 
     * @param deltaTime
     *            (float) czas trwania klatki w ms
     * @param elapsedTime
     *            (float) czas jaki do tej pory upłynął w scenie.
     */
    public abstract void updateScene(float deltaTime, long elapsedTime);

    /**
     * Metoda wywoływana w momencie aktywaności fizycznego przycisku.
     * 
     * @return Należy zwrócić true jeśli akcja została obsłużona, w przeciwnym
     *         przypadku false.
     */
    public abstract Boolean onButtonDown(int keyCode, KeyEvent event);
}
